Remove RETURNDATACOPY elimination from UnusedStoreEliminator#16508
Remove RETURNDATACOPY elimination from UnusedStoreEliminator#16508
RETURNDATACOPY elimination from UnusedStoreEliminator#16508Conversation
There was a problem hiding this comment.
Looks like removing this did not affect gas in any of our semantic tests?
Please check gas benchmarks from external tests though.
There was a problem hiding this comment.
relative
ir-no-optimize
| project | bytecode_size | deployment_gas | method_gas |
|---|---|---|---|
| brink | 0% |
||
| colony | 0% |
||
| elementfi | 0% |
||
| ens | 0% |
||
| euler | |||
| gnosis | |||
| gp2 | 0% |
||
| pool-together | 0% |
||
| uniswap | 0% |
+0% |
+0% |
| yield_liquidator | 0% |
+0% |
+0% |
| zeppelin |
ir-optimize-evm+yul
| project | bytecode_size | deployment_gas | method_gas |
|---|---|---|---|
| brink | 0% |
||
| colony | 0% |
||
| elementfi | 0% |
||
| ens | 0% |
0% |
0% |
| euler | 0% |
-0% |
+0% |
| gnosis | |||
| gp2 | 0% |
||
| pool-together | 0% |
||
| uniswap | 0% |
+0% |
-0% |
| yield_liquidator | 0% |
-0% |
0% |
| zeppelin | 0% |
-0% |
+0% |
ir-optimize-evm-only
| project | bytecode_size | deployment_gas | method_gas |
|---|---|---|---|
| brink | 0% |
||
| colony | 0% |
||
| elementfi | 0% |
||
| ens | 0% |
0% |
0% |
| euler | |||
| gnosis | |||
| gp2 | 0% |
||
| pool-together | 0% |
||
| uniswap | 0% |
0% |
+0% |
| yield_liquidator | 0% |
0% |
0% |
| zeppelin | 0% |
legacy-no-optimize
| project | bytecode_size | deployment_gas | method_gas |
|---|---|---|---|
| brink | 0% |
||
| colony | 0% |
||
| elementfi | 0% |
||
| ens | 0% |
||
| euler | 0% |
+0% |
-0% |
| gnosis | 0% |
||
| gp2 | 0% |
||
| pool-together | 0% |
||
| uniswap | 0% |
+0% |
+0% |
| yield_liquidator | 0% |
-0% |
0% |
| zeppelin |
legacy-optimize-evm+yul
| project | bytecode_size | deployment_gas | method_gas |
|---|---|---|---|
| brink | 0% |
||
| colony | 0% |
||
| elementfi | 0% |
||
| ens | 0% |
0% |
0% |
| euler | 0% |
-0% |
-0% |
| gnosis | 0% |
||
| gp2 | 0% |
||
| pool-together | 0% |
||
| uniswap | 0% |
+0% |
-0% |
| yield_liquidator | 0% |
-0% |
0% |
| zeppelin | 0% |
+0% |
+0% |
legacy-optimize-evm-only
| project | bytecode_size | deployment_gas | method_gas |
|---|---|---|---|
| brink | 0% |
||
| colony | 0% |
||
| elementfi | 0% |
||
| ens | 0% |
0% |
0% |
| euler | 0% |
+0% |
-0% |
| gnosis | 0% |
||
| gp2 | 0% |
||
| pool-together | 0% |
||
| uniswap | 0% |
-0% |
+0% |
| yield_liquidator | 0% |
0% |
0% |
| zeppelin |
!V = version mismatch
!B = no value in the "before" version
!A = no value in the "after" version
!T = one or both values were not numeric and could not be compared
-0 = very small negative value rounded to zero
+0 = very small positive value rounded to zero
absolute
ir-no-optimize
| project | bytecode_size | deployment_gas | method_gas |
|---|---|---|---|
| brink | 0 | ||
| colony | 0 | ||
| elementfi | 0 | ||
| ens | 0 | ||
| euler | |||
| gnosis | |||
| gp2 | 0 | ||
| pool-together | 0 | ||
| uniswap | 0 | 36 | 82 |
| yield_liquidator | 0 | 12 | 72 |
| zeppelin |
ir-optimize-evm+yul
| project | bytecode_size | deployment_gas | method_gas |
|---|---|---|---|
| brink | 0 | ||
| colony | 0 | ||
| elementfi | 0 | ||
| ens | 0 | 0 | 0 |
| euler | 0 | -24 | 5972 |
| gnosis | |||
| gp2 | 0 | ||
| pool-together | 0 | ||
| uniswap | 0 | 48 | -103 |
| yield_liquidator | 0 | -36 | 0 |
| zeppelin | 0 | -24 | 121 |
ir-optimize-evm-only
| project | bytecode_size | deployment_gas | method_gas |
|---|---|---|---|
| brink | 0 | ||
| colony | 0 | ||
| elementfi | 0 | ||
| ens | 0 | 0 | 0 |
| euler | |||
| gnosis | |||
| gp2 | 0 | ||
| pool-together | 0 | ||
| uniswap | 0 | 0 | 3955 |
| yield_liquidator | 0 | 0 | 0 |
| zeppelin | 0 |
legacy-no-optimize
| project | bytecode_size | deployment_gas | method_gas |
|---|---|---|---|
| brink | 0 | ||
| colony | 0 | ||
| elementfi | 0 | ||
| ens | 0 | ||
| euler | 0 | 12 | -12110 |
| gnosis | 0 | ||
| gp2 | 0 | ||
| pool-together | 0 | ||
| uniswap | 0 | 36 | 380 |
| yield_liquidator | 0 | -12 | 0 |
| zeppelin |
legacy-optimize-evm+yul
| project | bytecode_size | deployment_gas | method_gas |
|---|---|---|---|
| brink | 0 | ||
| colony | 0 | ||
| elementfi | 0 | ||
| ens | 0 | 0 | 0 |
| euler | 0 | -48 | -18878 |
| gnosis | 0 | ||
| gp2 | 0 | ||
| pool-together | 0 | ||
| uniswap | 0 | 48 | -1614 |
| yield_liquidator | 0 | -12 | 0 |
| zeppelin | 0 | 100 | 1426 |
legacy-optimize-evm-only
| project | bytecode_size | deployment_gas | method_gas |
|---|---|---|---|
| brink | 0 | ||
| colony | 0 | ||
| elementfi | 0 | ||
| ens | 0 | 0 | 0 |
| euler | 0 | 24 | -10010 |
| gnosis | 0 | ||
| gp2 | 0 | ||
| pool-together | 0 | ||
| uniswap | 0 | -12 | 882 |
| yield_liquidator | 0 | 0 | 0 |
| zeppelin |
!V = version mismatch
!B = no value in the "before" version
!A = no value in the "after" version
!T = one or both values were not numeric and could not be compared
-0 = very small negative value rounded to zero
+0 = very small positive value rounded to zero
761080c to
a3869e1
Compare
clonker
left a comment
There was a problem hiding this comment.
Looks good to me aside from the introduced error!
a3869e1 to
f817e10
Compare
f817e10 to
aca08f7
Compare
cameel
left a comment
There was a problem hiding this comment.
The main blocker for this is that it's missing regression tests for the bug. There are also some things to clarify about the buglist entry.
| "link": "https://blog.soliditylang.org/2026/X/Y/Z/", | ||
| "introduced": "0.8.13", | ||
| "fixed": "0.8.35", | ||
| "severity": "low", |
There was a problem hiding this comment.
Did we already decide on severity for this?
I would not give it more than very low, but we also need to agree on that as a team.
There was a problem hiding this comment.
I didn't know that there is something below low severity. Change it to very low. Can we just agreed here?
There was a problem hiding this comment.
Severity levels are listed in the docs: List of Known Bugs
To agree on one we normally discuss it on a call or on the security channel.
| Bugfixes: | ||
| * Yul: Fix incorrect serialization of Yul object names containing double quotes and escape sequences, producing output that could not be parsed as valid Yul. | ||
| * Yul EVM Code Transform: Improve stack shuffler performance by fixing a BFS deduplication issue. | ||
| * Yul Optimizer: Fix `UnusedStoreEliminator` incorrectly removing `returndatacopy` operations when the length comes from a stale `returndatasize()` call that was invalidated by subsequent call opcodes. |
There was a problem hiding this comment.
Entries for important bugs have their own section (that we only add when such bugfixes are present). Check how it was done for earlier releases.
There was a problem hiding this comment.
Moved, but do we define this as important bug? How do we define them then?
There was a problem hiding this comment.
If it gets a bug list entry it's an important bug.
There was a problem hiding this comment.
We also always put this category at the top.
aca08f7 to
9e623e9
Compare
9e623e9 to
fff3d3f
Compare
a2a9123 to
9d4c1d3
Compare
9d4c1d3 to
8b6f9fb
Compare
8b6f9fb to
b432744
Compare
cameel
left a comment
There was a problem hiding this comment.
The main problem I still see here is that we don't have a repro for the bug. The included command-line test shows only that returndatacopy() in inline assembly was being removed, not that it could be removed wrongly due to stale returndatasize(). There's also no evmasm repro. The whole idea of this being an important bug in the first place hinges on it actually affecting users :)
Other than that, a few small nitpicks.
| @@ -1,3 +1,4 @@ | |||
| // This optimisation step is removed and `returndatacopy` is never removed by `unusedStoreEliminator`. | |||
There was a problem hiding this comment.
"optimisation step" sounds like you're talking about the whole UnusedStoreEliminator. I'd explain it like this:
| // This optimisation step is removed and `returndatacopy` is never removed by `unusedStoreEliminator`. | |
| // Unused `returndatacopy()` is not supposed be optimized out in this case. | |
| // In fact, it's never optimized out now that we removed this optimization from UnusedStoreEliminator. |
It would fit the other test where you have this comment as well.
| @@ -0,0 +1,12 @@ | |||
| // SPDX-License-Identifier: GPL-3.0 | |||
| pragma solidity >=0.8.0; | |||
There was a problem hiding this comment.
We don't hard-code versions in these.
| pragma solidity >=0.8.0; | |
| pragma solidity *; |
There was a problem hiding this comment.
Also, I'd group it with the other Yul optimizer tests: unusedStoreEliminator_returndatacopy_returndatasize/ -> yul_optimizer_unused_store_eliminator_unused_returndatasize_removal/
And the comment should also say that returndatasize() used to be removed but now the expectation is that now it is not. This is not obvious without context and the name does not make it clear either.
| assembly { | ||
| // Destination offset 96 is chosen to make the written memory region [96, 96+returndatasize()) | ||
| // provably disjoint from the ABI encoder's mload(64) (which reads [64, 96)). | ||
| returndatacopy(96, 0, returndatasize()) |
There was a problem hiding this comment.
This verifies that the returndatacopy removal in inline assembly as possible and is no longer the case. Which is fine to have as a test, but not what I meant in #16508 (comment).
I expected something like the repro from the report. Which I guess would be this:
contract C {
function f() public view {
assembly {
let s := returndatasize()
pop(staticcall(gas(), 0x01, 0, 0, 0, 0))
returndatacopy(0, 0, s)
}
}
}I see that this does not reproduce the error though. Not even with the sequence we use in Yul tests for UnusedStoreEliminator (oxaSVj). Note also that this repro does not even compile - I had to remove the last argument from staticcall() to make it compile.
This puts into question if the bug can even be triggered. A buggy path definitely exists, but if there's no input that can exercise it, I would not classify it as an important bug.
There was a problem hiding this comment.
Looks like the repro does work in pure Yul:
{
let s := returndatasize()
pop(staticcall(gas(), 0x01, 0, 0, 0, 0))
returndatacopy(0, 0, s)
}So the bug is real after all. But the question still remains if it's reproducible in inline assembly. If it's not then I'd still consider demoting it.
We also still need a non-Yul version of the test to prove that affects evmasm as well (otherwise buglist entry needs viaIR: true).
There was a problem hiding this comment.
BTW, I'd put the repro under yul_optimizer_unused_store_eliminator_stale_returndatasize_bug/ (Solidity version) and strict_asm_yul_optimizer_unused_store_eliminator_stale_returndatasize_bug/ (Yul version).
There was a problem hiding this comment.
I added semantic test which triggers the bug.
There was a problem hiding this comment.
Will move it to cmdLineTests.
There was a problem hiding this comment.
Actually I think we should create the tests as you mentioned but the semantic test should stay too. It checks that the call reverts. cmdLineTest only verifies that the returndataload is not removed by the optimizer step.
| "name": "UnusedStoreEliminatorStaleReturnDataSize", | ||
| "summary": "The Yul optimizer's ``UnusedStoreEliminator`` may incorrectly remove ``returndatacopy(...)`` operations when using a stale value from ``returndatasize()`` that was invalidated by subsequent call operations.", | ||
| "description": "The ``UnusedStoreEliminator`` is a Yul optimizer step that removes redundant memory and storage writes. One of the operations eligible for removal is ``returndatacopy(...)``. This particular operation has a quirk - unlike any other instruction for bulk memory copying it reverts on out-of-bounds access. A revert is one of the side-effects that the optimizer guarantees to preserve so the operation can only be removed when it is certain that it cannot revert. This is the case when the entire return data buffer is copied to memory, i.e. when the start offset is zero and the length equals ``returndatasize()``. The optimizer was special-cased to detect and optimize only this specific pattern, since it matches the code produced by the code generator for external calls. However, the check did not account for the possibility of ``returndatasize()`` values becoming stale. The size of the return data buffer is updated by ``call()``, ``staticcall()``, ``delegatecall()``, and ``callcode()``. If a ``returndatasize()`` value is stored in a variable before such an operation and then used in a subsequent ``returndatacopy(...)``, the stored size may no longer reflect the actual return data buffer size. Despite this, the optimizer would consider it safe to remove, bypassing the revert and allowing the code to continue, possibly leading to unexpected behavior. Since the code generator never produces code that interleaves multiple calls and access to their return data, the bug only affected inline assembly or handwritten Yul code. The necessary condition is the use of an optimizer sequence including the ``UnusedStoreEliminator`` step (which is the default).", | ||
| "link": "https://blog.soliditylang.org/2026/04/21/unusedstore-eliminator-stale-returndatasize-bug/", |
There was a problem hiding this comment.
The release is currently planned for the 29th.
| "link": "https://blog.soliditylang.org/2026/04/21/unusedstore-eliminator-stale-returndatasize-bug/", | |
| "link": "https://blog.soliditylang.org/2026/04/29/unusedstore-eliminator-stale-returndatasize-bug/", |
This should be corrected in the blog post as well.
18eafa4 to
a15d018
Compare
a15d018 to
257d0fc
Compare
As discussed here we decided to remove this optimisation as it has a minimal impact on generated code and make the implementation of the
UnusedStoreEliminatorcomplicated and bug prone. In this case patternreturndatacopy(0, 0, returndatasize())will never be eliminated.